#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#include <stdarg.h>
#include <string.h>

#include "util.h"
#include "inputcmd-server.h"

/** 
 * internal input command handler info which is inserted into hash table
 */
typedef struct {
    NutstoreInputCommandHandler     handler;
    gboolean    b_add_idle;     // True, run handler in mail loop thread; 
                                // False, run handler in the server thread
    gpointer    user_data;
} InputCommandHandlerInfo;

/**
 * free command resource
 */
static void nutstore_inputcmd_free(NutstoreInputCommand *cmd)
{
    if (cmd) {
        g_free(cmd->command_name);
        g_hash_table_unref(cmd->command_args);
        g_free(cmd);
    }
}

/**
 * parse input command from message
 * you MUST free the returned command data after using it
 */
NutstoreInputCommand * inputcmd_parse_from_message(const gchar *msg)
{
    NutstoreInputCommand *cmd;
    gchar **lines;
    guint num_lines;
    guint i;
    gboolean retval;

    // Valid input command format:
    //      {command_name}\n
    //      {key0}\t{value0}\t{value1}\t...\t{valueN}\n
    //      {key1}\t{value0}\t{value1}\t...\t{valueN}\n
    //      ...
    //      {keyN}\t{value0}\t{value1}\t...\t{valueN}\n
    //      done\n
    lines = g_strsplit(msg, "\n", 0);
    num_lines = g_strv_length(lines);

    // command must have "{command_name}\n" and "done\n" at least.
    if (num_lines < 3) {
        debug("Input command error: too few lines");
        goto INVALID_MESSAGE;
    } 

    // All input command must start with not-empty command name.
    if (strlen(lines[0]) <= 0) {
        debug("Input command error: empty command name");
        goto INVALID_MESSAGE;
    }

    // All input command must end with "done\n".
    if (strcmp(lines[num_lines-2], "done") != 0 || strlen(lines[num_lines-1]) != 0) {
        debug("Input command error: not ends with \"done\\n\"");
        goto INVALID_MESSAGE;
    }

    // create new command
    cmd = g_new0(NutstoreInputCommand, 1);

    cmd->command_name = g_strdup(lines[0]);
    cmd->command_args = g_hash_table_new_full((GHashFunc) g_str_hash,
            (GEqualFunc) g_str_equal,
            (GDestroyNotify) g_free,
            (GDestroyNotify) g_strfreev); 

    cmd->handler = NULL;
    cmd->user_data = NULL;

    // add key/value arguments
    for (i = 1; i < num_lines - 2; i++) {
        retval = nutstore_util_command_parse_arg(lines[i], cmd->command_args);
        if (retval == FALSE) {
            debug("Input command error: parse args failed");
            nutstore_inputcmd_free(cmd);
            goto INVALID_MESSAGE;
        }
    }

    g_strfreev(lines);
    return cmd;

INVALID_MESSAGE:
    g_strfreev(lines);
    return NULL;
}

/** 
 * execute the command handler in the gui thread. After the handler is
 * invoked, the command is freed. So, the command handler should never free
 *  the command 
 */
static gboolean nutstore_inputcmd_run_async(NutstoreInputCommand *cmd)
{
    g_assert(cmd->handler != NULL);

    // call user supplied handler 
    (cmd->handler)(cmd);

    // free command data safely
    nutstore_inputcmd_free(cmd);

    // MUST return FALSE to remove from the main loop.
    return FALSE;
}
/**
 * run the command
 */
static void 
nutstore_inputcmd_server_run_command(NutstoreInputCommandServer *server,
        NutstoreInputCommand *cmd)
{
    InputCommandHandlerInfo *info;

    info = (InputCommandHandlerInfo *)g_hash_table_lookup(server->dispatch_table, 
            cmd->command_name);
    if (info == NULL) {
        debug("recv unkown UDP command:%s", cmd->command_name);
        nutstore_inputcmd_free(cmd);
        return;
    }
    
    if (info->handler != NULL) {
        cmd->handler = info->handler;
        cmd->user_data = info->user_data;

        if (info->b_add_idle) {
            g_idle_add((GSourceFunc)nutstore_inputcmd_run_async, cmd);
        } else {
            (info->handler)(cmd);
            nutstore_inputcmd_free(cmd);
        } 
    } else {
        nutstore_inputcmd_free(cmd);
    }
}

/**
 * Setup input command server
 */
void nutstore_inputcmd_server_setup(NutstoreInputCommandServer *server)
{
    server->dispatch_table = g_hash_table_new_full((GHashFunc) g_str_hash,
            (GEqualFunc) g_str_equal,
            (GDestroyNotify) g_free,
            (GDestroyNotify) g_free);
    
    g_hook_list_init(&(server->onconnect_hooklist), sizeof(GHook));
    g_hook_list_init(&(server->ondisconnect_hooklist), sizeof(GHook));
}

/**
 * try connecting to daemon
 */
static gboolean create_socket(int *pSocket)
{
    int sock = -1;
    struct sockaddr_in addr;
    struct timeval tv = { 3, 0 };   // Timeout: 3 seconds
    int opt_true = 1;
    int ret;

    /* intialize address structure */
    addr.sin_family = AF_INET;
    addr.sin_port = htons(INPUT_COMMAND_SERVER_PORT);
    inet_aton("127.0.0.1", &addr.sin_addr);

    // Using UDP 
    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock < 0) {
        debug("UDP socket() failed");
        goto CREATE_SOCKET_FAIL;
    }

    /* enable address reuse before bind() */
    ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt_true, sizeof(opt_true));
    if (ret != 0) {
        debug("UDP setsockopt() SO_REUSEADDR failed\n");
        goto CREATE_SOCKET_FAIL;
    }

    ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) {
        debug("UDP bind() failed");
        goto CREATE_SOCKET_FAIL;
    }

    /* We do not need to set timeout value on UDP socket */
    //ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval));
    //if (ret < 0) {
    //    debug("UDP setsockeopt() RCVTIMEO failed");
    //    goto CREATE_SOCKET_FAIL;
    //}

    ret = setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(struct timeval));
    if (ret < 0) {
        debug("UDP setsockeopt() SNDTIMEO failed");
        goto CREATE_SOCKET_FAIL;
    }

    *pSocket = sock;
    return TRUE;

CREATE_SOCKET_FAIL:
    if (sock >= 0) {
        close(sock);
    }
    return FALSE;
}

/**
 * server thread function
 */
static gpointer nutstore_inputcmd_server_thread(NutstoreInputCommandServer *server)
{   
#define MAX_UDP_MSG_SIZE    (2048)
    gchar * buf = g_new0(gchar, MAX_UDP_MSG_SIZE);
   
    while (1) {
        // try to create UDP socket
        int sock;

        if (create_socket(&sock) == FALSE) {
            // wait 2 seconds before retrying again
            g_usleep(2 * G_USEC_PER_SEC);
            continue;
        }

        debug("UDP input command server created\n"); 

        // listen for UDP message
        while (1) {
            NutstoreInputCommand *cmd;
            int n;

            n = recvfrom(sock, buf, MAX_UDP_MSG_SIZE-1, 0, NULL, NULL);
            if (n == -1) {  // error
                close(sock);
                break;
            } else if (n == 0) { // the peer has closed the connection
                // This is UDP socket, we should not be here
                g_assert_not_reached();
                continue;
            }

            // normal data
            buf[n] = '\0';  // make sure message end

            debug("recv UDP data (len=%d):\n%s", n, buf);

            cmd = inputcmd_parse_from_message(buf); 
            if (cmd != NULL) {
                nutstore_inputcmd_server_run_command(server, cmd);
            } else {
                // Invalid command? -- nothing to do 
            }
        }
    }
  
    g_free(buf);

#undef MAX_UDP_MSG_SIZE
 
    return NULL;
}

/**
 * start input command server
 */
void nutstore_inputcmd_server_start(NutstoreInputCommandServer *server)
{
    debug("starting input command thread\n");
    g_thread_create((gpointer (*)(gpointer)) nutstore_inputcmd_server_thread,
            server, FALSE, NULL);
}

/**
 * Add input command handler
 */
void nutstore_inputcmd_server_add_handler(NutstoreInputCommandServer *server,
        const gchar *command_name,
        NutstoreInputCommandHandler handler,
        gboolean b_add_idle,
        gpointer user_data)
{
    InputCommandHandlerInfo *info;

    info = g_new0(InputCommandHandlerInfo, 1);

    info->handler = handler;
    info->b_add_idle = b_add_idle;
    info->user_data = user_data;

    g_hash_table_insert(server->dispatch_table, g_strdup(command_name), info);
}

/**
 * add connection state change hook
 */
void nutstore_inputcmd_server_add_on_disconnect_hook(NutstoreInputCommandServer *server,
        NutstoreInputCommandServerConnectHook func,
        gpointer user_data) 
{
    GHook *newhook;

    newhook = g_hook_alloc(&(server->ondisconnect_hooklist));
    newhook->func = func;
    newhook->data = user_data;

    g_hook_append(&(server->ondisconnect_hooklist), newhook);
}

void
nutstore_inputcmd_server_add_on_connect_hook(NutstoreInputCommandServer *server,
        NutstoreInputCommandServerConnectHook func,
        gpointer user_data) 
{
    GHook *newhook;

    newhook = g_hook_alloc(&(server->onconnect_hooklist));
    newhook->func = func;
    newhook->data = user_data;

    g_hook_append(&(server->onconnect_hooklist), newhook);
}

